package org.springframework.data.jpa.repository.query;

import java.lang.reflect.Method;

import javax.persistence.EntityManager;

import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.PartTreeJpaQuery;
import org.springframework.data.jpa.repository.query.QueryExtractor;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;

public final class CommonJpaQueryLookupStrategy
{
    private CommonJpaQueryLookupStrategy()
    {
    }
    
    /**
     * Base class for {@link QueryLookupStrategy} implementations that need access to an {@link EntityManager}.
     * 
     * @author Oliver Gierke
     */
    private abstract static class AbstractQueryLookupStrategy implements QueryLookupStrategy
    {
        
        private final EntityManager em;
        
        private final QueryExtractor provider;
        
        public AbstractQueryLookupStrategy(EntityManager em, QueryExtractor extractor)
        {
            
            this.em = em;
            this.provider = extractor;
        }
        
        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.data.repository.query.QueryLookupStrategy# resolveQuery(java.lang.reflect.Method,
         * org.springframework.data.repository.core.RepositoryMetadata,
         * org.springframework.data.repository.core.NamedQueries)
         */
        public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries)
        {
            
            return resolveQuery(new JpaQueryMethod(method, metadata, provider), em, namedQueries);
        }
        
        protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em,
            NamedQueries namedQueries);
    }
    
    /**
     * {@link QueryLookupStrategy} to create a query from the method name.
     * 
     * @author Oliver Gierke
     */
    private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy
    {
        
        public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor)
        {
            
            super(em, extractor);
        }
        
        @Override
        protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries)
        {
            
            try
            {
                return new PartTreeJpaQuery(method, em);
            }
            catch (IllegalArgumentException e)
            {
                throw new IllegalArgumentException(String.format("Could not create query metamodel for method %s!",
                    method.toString()), e);
            }
        }
    }
    
    /**
     * {@link QueryLookupStrategy} that tries to detect a declared query declared via {@link Query} annotation followed
     * by a JPA named query lookup.
     * 
     * @author Oliver Gierke
     */
    private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy
    {
        
        public DeclaredQueryLookupStrategy(EntityManager em, QueryExtractor extractor)
        {
            
            super(em, extractor);
        }
        
        @Override
        protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries)
        {
            RepositoryQuery query = CommonSimpleJpaQuery.fromQueryAnnotation(method, em);
            
            if (null != query)
            {
                return query;
            }
            
            String name = method.getNamedQueryName();
            if (namedQueries.hasQuery(name))
            {
                return new CommonSimpleJpaQuery(method, em, namedQueries.getQuery(name));
            }
            
            query = NamedQuery.lookupFrom(method, em);
            
            if (null != query)
            {
                return query;
            }
            
            throw new IllegalStateException(
                String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
        }
        
    }
    
    /**
     * {@link QueryLookupStrategy} to try to detect a declared query first (
     * {@link org.springframework.data.jpa.repository.Query}, JPA named query). In case none is found we fall back on
     * query creation.
     * 
     * @author Oliver Gierke
     */
    private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLookupStrategy
    {
        
        private final DeclaredQueryLookupStrategy strategy;
        
        private final CreateQueryLookupStrategy createStrategy;
        
        public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor)
        {
            
            super(em, extractor);
            this.strategy = new DeclaredQueryLookupStrategy(em, extractor);
            this.createStrategy = new CreateQueryLookupStrategy(em, extractor);
        }
        
        @Override
        protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries)
        {
            
            try
            {
                return strategy.resolveQuery(method, em, namedQueries);
            }
            catch (IllegalStateException e)
            {
                return createStrategy.resolveQuery(method, em, namedQueries);
            }
        }
    }
    
    /**
     * Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and {@link Key}.
     * 
     * @param em
     * @param key
     * @return
     */
    public static QueryLookupStrategy create(EntityManager em, Key key, QueryExtractor extractor)
    {
        
        if (key == null)
        {
            return new CreateIfNotFoundQueryLookupStrategy(em, extractor);
        }
        
        switch (key)
        {
            case CREATE:
                return new CreateQueryLookupStrategy(em, extractor);
            case USE_DECLARED_QUERY:
                return new DeclaredQueryLookupStrategy(em, extractor);
            case CREATE_IF_NOT_FOUND:
                return new CreateIfNotFoundQueryLookupStrategy(em, extractor);
            default:
                throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
        }
    }
}